<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    <!-- 引入element 的组件库-->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <!--引入 element-ui 的样式，-->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.3.2/dist/html2canvas.min.js"></script>
    <style>
        :root {
            --primary: #6c63ff;
            --secondary: #4a44b5;
            --accent: #ff6584;
            --light: #f8f9fa;
            --dark: #212529;
            --gray: #6c757d;
            --light-gray: #e9ecef;
            --transition: all 0.3s ease;
            --shadow: 0 5px 15px rgba(0,0,0,0.1);
            --shadow-hover: 0 8px 25px rgba(0,0,0,0.15);
            --poly: 20;
            --canvansWidth: 400;
            --canvansHeight: 400;
        }
        .el-form-item {
            margin-bottom: 5px;
        }
        .header-title {
            background: rgba(255, 255, 255, 0.95);
            box-shadow: var(--shadow);
            position: sticky;
            top: 0;
            z-index: 100;
            backdrop-filter: blur(10px);
        }
        .wrap {
            display: flex;
            flex-direction: row;
        }
        .config {
            width: 35vh;
            padding-right: 5px;
        }
        .px-container {
            display: flex;
            flex: 1;
            flex-direction: column;
        }
        #pxContainer {
            position: relative;
            height: calc((var(--canvansHeight) + var(--poly)) * 1px);
            width: calc((var(--canvansWidth) + var(--poly)) * 1px);
        }
        #canvas {
            top: calc(var(--poly) * 1px);
            left: calc(var(--poly) * 1px);
            position: absolute;
            width: calc(var(--canvansWidth) * 1px);
            height: calc(var(--canvansHeight) * 1px);
        }
        #numContainer {
            position: absolute;
            display: flex;
            flex-wrap:  wrap;
            top: calc(var(--poly) * 1px);
            left: calc(var(--poly) * 1px);
        }
        .num-cell {
            font-size: 5px;
            width: calc(var(--poly) * 1px);
            height: calc(var(--poly) * 1px);
            text-shadow: 0px 0px 0px gray;
            -webkit-text-stroke: 0.1px gray;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .avatar-uploader .el-upload {
            border: 1px dashed #d9d9d9;
            border-radius: 6px;
            cursor: pointer;
            position: relative;
            overflow: hidden;
        }
        .avatar-uploader .el-upload:hover {
            border-color: #409EFF;
        }
        .avatar-uploader-icon {
            font-size: 28px;
            color: #8c939d;
            width: 40px;
            height: 40px;
            line-height: 40px;
            text-align: center;
        }
        .avatar {
            width: 40px;
            height: 40px;
            display: block;
        }
        .color-list {
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
        }
        .color-item {
            width: 20%;
            height: 20px;
        }
        .top, .left {
            display: flex;
            div {
                width: calc(var(--poly) * 1px);
                height: calc(var(--poly) * 1px);
                font-size: 5px;
                text-align: center;
                display: flex;
                align-items: center;
                justify-content: center;
            }
        }
        .top {
            width: calc(var(--canvansWidth) * 1px);
            position: absolute;
            left: calc(var(--poly) * 1px);
        }
        .left {
            height: calc(var(--canvansHeight) * 1px);
            flex-direction: column;
            position: absolute;
            top: var(--poly);
        }
        .btns {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 10px;
        }
    </style>
</head>
<body>

<div id="app">
<!--    拼豆图纸生成器-->
    <header><h1 class="header-title"></h1></header>
    <main class="wrap">
<!--        像素图纸-->
        <section class="px-container">
            <div  id="pxContainer">
                <div class="left">
                    <div v-for="(item,index) in canvasItemH">{{item}}</div>
                </div>
                <div>
                    <div class="top">
                        <div v-for="(item,index) in canvasItemW">{{item}}</div>
                    </div>
                    <canvas id="canvas"></canvas>
                </div>
                <div id="numContainer" v-show="showNum === 1"></div>
            </div>
        </section>
<!--        像素图像设置-->
        <section class="config">
            <el-form label-position="top">
                <el-form-item label="上传图片">
                    <el-upload
                            class="avatar-uploader"
                            action="https://jsonplaceholder.typicode.com/posts/"
                            :show-file-list="false"
                            :on-success="handleAvatarSuccess"
                            :before-upload="beforeAvatarUpload">
                        <img v-if="imageUrl" ref="source" :src="imageUrl" class="avatar" id="source">
                        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
                    </el-upload>
                </el-form-item>
                <el-form-item label="图片宽度">
                    <el-input size="mini" v-model="width" type="number" min="300">
                        <template slot="append">px</template>
                    </el-input>
                </el-form-item>
                <el-form-item label="图片高度">
                    <el-input size="mini" v-model="height" type="number" min="300">
                        <template slot="append">px</template>
                    </el-input>
                </el-form-item>
                <el-form-item label="像素化单元格大小">
                    <el-input size="mini" v-model="poly" type="number" min="10">
                        <template slot="append">px</template>
                    </el-input>
                </el-form-item>
                <el-form-item label="展示颜色编号">
                    <el-radio-group v-model="showNum">
                        <el-radio :label="1">展示</el-radio>
                        <el-radio :label="0">隐藏</el-radio>
                    </el-radio-group>
                </el-form-item>
                <div class="btns">
                    <el-button size="mini" @click="toPx" type="primary" :disabled="!imgFinish">生成</el-button>
                    <el-button size="mini" @click="getColorList" type="primary" :disabled="!canvas">颜色对应表</el-button>
                    <el-button size="mini" @click="exportAsImage('pxContainer','像素图纸')" type="primary" :disabled="!canvas">导出像素图片</el-button>
                </div>
            </el-form>
        </section>
<!--        颜色对应表-->
        <el-dialog
                title="颜色对应表"
                :visible.sync="dialogVisible"
                width="80%">
            排序：<el-radio-group v-model="radio" @change="changeSort">
            <el-radio :label="1">按16进制</el-radio>
            <el-radio :label="2">按编号</el-radio>
        </el-radio-group>
            <div class="color-list" id="colorList">
                <div v-for="item in colorKeys" class="color-item" :style="{'background-color': `${item[0]}`}">
                    {{item[1]}}:{{item[0]}}
                </div>
            </div>
            <div class="btns">
                <el-button size="mini" @click="exportAsImage('colorList','颜色对应表')" type="primary" :disabled="!canvas">导出颜色表</el-button>
            </div>
        </el-dialog>
    </main>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            showNum: 1,
            radio: 2,
            imageUrl: '',
            poly: 20,
            width: 400,
            height: 400,
            image: null,
            canvas: null,
            canvasData: null,
            ctx: null,
            count: 0,
            yCount: 0,
            colorList: [],
            colorKeys: [],
            trueColorMap: {},
            numCount: 0,
            imgFinish: '',
            dialogVisible: false,
            canvasItemW: 30,
            canvasItemH: 30
        },
        watch: {
            showNum: {
                handler(val) {
                   if(val === 1) {
                       const numContainer = document.getElementById('numContainer')
                       if(numContainer) {
                           numContainer.innerHTML = ''
                           this.showPxNum()
                       }
                   }
                }
            }
        },
        methods: {
            exportAsImage(id, fileName) {
                const element = document.getElementById(id)
                html2canvas(element).then(function(canvas) {
                    const link = document.createElement("a")
                    link.href = canvas.toDataURL("image/png")
                    link.download = `${fileName}.png`
                    link.click();
                })
            },
            handleAvatarSuccess(res, file) {
                this.imgFinish = false
                this.imageUrl = URL.createObjectURL(file.raw);
                this.imgFinish = true

            },
            beforeAvatarUpload(file) {
                const isJPG = file.type === 'image/jpeg';
                const isLt2M = file.size / 1024 / 1024 < 2;

                if (!isJPG) {
                    this.$message.error('上传头像图片只能是 JPG 格式!');
                }
                if (!isLt2M) {
                    this.$message.error('上传头像图片大小不能超过 2MB!');
                }
                return isJPG && isLt2M;
            },
            changeSort() {
                const colorMap = Object.entries(this.trueColorMap)
                if(this.radio === 1) {
                    this.colorKeys = colorMap.sort()
                } else {
                    this.colorKeys = colorMap.sort((a,b) =>
                    {
                        let firstEle = a[1].substring(0, 1), firstEle2 = b[1].substring(0, 1)
                        let num = a[1].substring(1), num2 = b[1].substring(1);
                        if (firstEle !== firstEle2) {
                            return firstEle.localeCompare(firstEle2)
                        } else {
                            return num < num2
                        }
                    })
                }

            },
            getColorList() {
                const colorMap = Object.entries(this.trueColorMap)
                this.colorKeys = colorMap.sort((a,b) =>
                {
                    let firstEle = a[1].substring(0, 1), firstEle2 = b[1].substring(0, 1)
                    let num = a[1].substring(1), num2 = b[1].substring(1);
                    if (firstEle !== firstEle2) {
                        return firstEle.localeCompare(firstEle2)
                    } else {
                        return num < num2
                    }
                })
                this.dialogVisible = true
            },
            toPx () {
                this.$nextTick(() => {
                    this.colorList = []
                    this.trueColorMap = {}
                    document.documentElement.style.setProperty('--poly', this.poly);
                    document.documentElement.style.setProperty('--canvansWidth', Number(this.width));
                    document.documentElement.style.setProperty('--canvansHeight', Number(this.height));
                    this.canvasItemW = this.width / this.poly
                    this.canvasItemH = this.height / this.poly
                    this.image = document.getElementById('source')
                    this.canvas = document.getElementById('canvas')
                    this.canvas.width = this.width
                    this.canvas.height = this.height
                    this.ctx = this.canvas.getContext('2d')
                    this.ctx.drawImage(this.image,0,0,this.width,this.height)
                    // 获取图像数据
                    this.canvasData = this.ctx.getImageData(0,0,this.width,this.height)
                    let area = {}
                    this.count = 0
                    this.yCount = 0
                    // 遍历canvas的每行像素块
                    for (let x = 0; x < this.canvas.height; x += Number(this.poly)) {
                        this.count++
                        // 遍历canvas第x行的每个像素块
                        for(let y = 0; y < this.canvas.width; y += Number(this.poly)) {
                            this.yCount++
                            // area的宽当前像素块加poly大于canvas的宽时，取canvas的宽和ploy的余，反之取poly
                            // area的宽当前像素块加poly大于canvas的高时，取canvas的高和ploy的余，反之取poly
                            area = {
                                w: (y+ Number(this.poly)) > this.canvas.width ? parseInt(this.canvas.width % this.poly) : Number(this.poly),
                                h: this.count * this.poly > this.canvas.height ? parseInt(this.canvas.height % this.poly) : Number(this.poly),

                            }
                            const idx = (y + x * this.canvas.width) * 4
                            this.averageColors(idx, area)
                        }
                    }
                    // 渲染像素图
                    this.ctx.putImageData(this.canvasData, 0 ,0)
                    const numContainer = document.getElementById('numContainer')
                    if(numContainer && this.showNum === 1) {
                        numContainer.innerHTML = ''
                        numContainer.style.width = this.width
                        numContainer.style.height = this.height
                        this.showPxNum()
                    }
                })
            },
            showPxNum() {
                // 获取使用到的颜色
                const pureColor = [...new Set(this.colorList)]
                let i = 65
                let j = 1
                const circleY = Math.ceil(pureColor.length / 24)
                // 为颜色编码
                pureColor.forEach(item => {
                    this.trueColorMap[item] = String.fromCharCode(i)+j
                    if(j < circleY) {
                        j++
                    } else {
                        i++
                        j = 1
                    }
                })
                // 将编码写到对应的像素上
                this.numCount = 0
                for ( let i = 0; i < this.width / this.poly; i++) {
                    for( let j = 0; j < this.height / this.poly; j++) {
                        this.addNum(this.trueColorMap[this.colorList[this.numCount]])
                        this.numCount++
                    }
                }
            },
            addColorMap(RGB) {
                this.colorList.push(RGB)
            },
            transformationHexadecimal(arr = [0, 0, 0]) {
                let strHex = ''
                for (let i = 0; i < arr.length; i++) {
                    let hex = '0' + arr[i].toString(16)
                    hex = hex.slice(-2)
                    strHex += hex
                }
                strHex = strHex.toUpperCase()
                return `#${strHex}`
            },
            averageColors(idx, area) {
                let aveR = this.aveColors(idx, area)
                let aveG = this.aveColors(idx + 1, area)
                let aveB = this.aveColors(idx + 2, area)
                this.addColorMap(this.transformationHexadecimal([parseInt(aveR), parseInt(aveG), parseInt(aveB)]))
                this.fullColors(idx, {aveR: aveR, aveG: aveG, aveB: aveB}, area)
            },
            aveColors(idx, area) {
                let total = 0
                for(let i = 0; i< area.h; i++) {
                    for(let j = 0;j< area.w; j++) {
                        if(this.canvasData.data[idx + (j* 4) + (this.width * i * 4)]) {
                            total += this.canvasData.data[idx + (j* 4) + (this.width * i * 4)]
                        }
                    }
                }
                return total / (area.w * area.h)
            },
            fullColors(idx, rgb, area) {
                for(let i = 0; i< area.h; i++) {
                    for(let j= 0; j< area.w; j++) {
                        this.canvasData.data[idx + 0 + (j*4) + (this.width * i * 4)] = rgb.aveR
                        this.canvasData.data[idx + 1 + (j*4) + (this.width * i * 4)] = rgb.aveG
                        this.canvasData.data[idx + 2 + (j*4) + (this.width * i * 4)] = rgb.aveB
                    }
                }
            },
            addNum(count) {
                const numContainer = document.getElementById('numContainer')
                let newEle = document.createElement('div')
                newEle.classList.add('num-cell')
                newEle.textContent = count
                numContainer.appendChild(newEle)
            },
        },
        mounted() {
            // this.toPx()
        }
    })








</script>
</body>
</html>
